import { consts } from "@/module-00-codebase/pkg-00-const";
import { Bi, Ei } from "@/module-00-codebase/pkg-01-container";
import { aw, pendingResolve, Throwable } from "@/module-00-codebase/pkg-02-tool";
import { Field } from "@/module-00-codebase/pkg-08-entity/field-entity";
import { RpcResult } from "@/module-00-codebase/pkg-08-entity/rpc-result";

export class Utils4React {
  public createGetter<T, THIS>(initValue: (this: THIS) => T, refreshValue?: (this: THIS, value: T) => T | undefined): (this: THIS) => T {
    let value: any = null;
    let inited = false;
    return function (this: THIS) {
      if (!inited && (inited = true)) value = initValue.call(this);
      if (!!refreshValue) (refreshVal => (value = refreshVal === undefined ? value : refreshVal))(refreshValue.call(this, value));
      return value;
    };
  }
  public upperFirst(str: string): string {
    return str.substring(0, 1).toUpperCase() + str.substring(1);
  }
  public lowerFirst(str: string): string {
    return str.substring(0, 1).toLowerCase() + str.substring(1);
  }
  public underScore(str: string): string {
    const chars = Array.from(str || "").filter(Boolean);
    return chars.slice(1).reduce((a, b) => `${a}${b.toLowerCase() === b ? "" : "_"}${b.toLowerCase()}`, (chars[0] || "").toLowerCase());
  }
  public bigCamelCase(underScoreStr: string) {
    return underScoreStr.split("_").reduce((a, b) => a + this.upperFirst(b.toLowerCase()), "");
  }
  public smlCamelCase(underScoreStr: string) {
    const bcc = this.bigCamelCase(underScoreStr);
    return this.lowerFirst(bcc);
  }
  public uuid() {
    return S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4();
  }
  public linkFuncs(...funcs: ((...args: any[]) => any)[]) {
    return function (this: any, ...args: any[]) {
      for (let i = 0; i < funcs.length - 1; i++) {
        const temp = funcs[i] || (emptyFunc as (arg: any) => any);
        temp.apply(this, args);
      }
      return funcs[funcs.length - 1].apply(this, args);
    };
  }
  public arr2map(datas: any[], keyGetter: (data: any) => string): { [key: string]: any } {
    const res = {} as { [key: string]: any };
    datas.forEach(d => (res[keyGetter(d)] = d));
    return res;
  }
  public cutRootProperty(propertyPath: string): string {
    const dotIndex = propertyPath.indexOf(".");
    if (dotIndex === 0) {
      return this.cutRootProperty(propertyPath.substring(1));
    } else if (dotIndex > 0) {
      return propertyPath.substring(0, dotIndex);
    } else {
      return propertyPath;
    }
  }
  public isNullOrEmpty(value: any) {
    const res = value === null || value === undefined || value === "" || (Array.isArray(value) && value.length === 0) || (value.construcotr === Object && Object.keys(value).length === 0);
    return res;
  }
  public async validateAll(attributes: any[]) {
    const promises = attributes.map(a => a.validate());
    const messages = [] as string[];
    for (let i = 0; i < promises.length; i++) {
      messages.push(await promises[i]);
    }
    return messages;
  }
  public deepEquals(a: any, b: any) {
    for (let x in a) {
      if (b[x] !== a[x]) {
        return false;
      }
    }
    return true;
  }
  public deepCopy<T>(obj: T, filter: (obj: any, property: string, value: any) => boolean = () => true, cache: Map<T, T> = new Map()): T {
    if (this.isBasicValue(obj)) {
      return obj;
    } else if (cache.has(obj)) {
      return cache.get(obj) as typeof obj;
    } else if (Array.isArray(obj)) {
      const res = new Array<any>();
      cache.set(obj, res as typeof obj);
      obj.forEach((o, i) => (res[i] = this.deepCopy(o, filter, cache)));
      return res as typeof obj;
    } else if (obj instanceof Set) {
      const res = new Set<any>() as typeof obj;
      cache.set(obj, res);
      Array.from(obj).forEach(o => res.add(this.deepCopy(o, filter, cache)));
      return res;
    } else if (obj instanceof Map) {
      const res = new Map<any, any>();
      cache.set(obj, res as typeof obj);
      obj.forEach((v, k) => res.set(this.deepCopy(k, filter, cache), this.deepCopy(v, filter, cache)));
      return res as typeof obj;
    } else {
      const res = {} as { [key: string]: any };
      cache.set(obj, res as typeof obj);
      let properties = Object.keys(obj);
      for (let i = 0; i < properties.length; i++) {
        let property = properties[i];
        let value = (obj as any)[property];
        if (!filter(obj, property, value)) {
          continue;
        } else {
          res[property] = this.deepCopy(value, filter, cache);
        }
      }
      return res as typeof obj;
    }
  }
  public isBasicValue(value: any): boolean {
    return !value || typeof value !== "object";
  }
  public toNotNullArray(arr: any) {
    return (Array.isArray(arr) ? arr : [arr]).filter(a => a !== null && a !== undefined);
  }
  public platingValue(obj: any) {
    const res = {} as any;
    const keys = Object.getOwnPropertyNames(obj);
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i];
      this.writeValue(res, key, obj[key]);
    }
    return res;
  }
  public readValue(root: any, propertyPath: string) {
    const properties = this.splitProperties(propertyPath);
    let obj = root;
    for (let i = 0; i < properties.length; i++) {
      const crrtKey = __parseProperty(properties[i]);
      obj = obj[crrtKey];
      if (obj === null || obj === undefined) {
        break;
      }
    }
    return obj;
  }
  public writeValue(root: any, propertyPath: string, value: any) {
    const properties = this.splitProperties(propertyPath);
    let obj = root;
    let nextKey = properties[0] as string | number;
    for (let i = 0; i < properties.length - 1; i++) {
      const crrtKey = __parseProperty(properties[i]);
      nextKey = __parseProperty(properties[i + 1]);
      if (obj[crrtKey] === null || obj[crrtKey] === undefined) {
        obj[crrtKey] = typeof nextKey === "number" ? [] : {};
      }
      obj = obj[crrtKey];
    }
    obj[nextKey] = value;
  }
  public notNullDelayCheck(getCacheValue: (ref: HTMLDivElement) => any[], start: (ref: HTMLDivElement) => void, finish: (ref: HTMLDivElement) => void) {
    const func = (ref: HTMLDivElement) => {
      if (!!ref) {
        start(ref);
        let count = 0;
        (async () => {
          Bi.log.debug(`check: ${++count}`);
          let cache = getCacheValue(ref);
          await aw(300);
          let lastCache = cache;
          const some = (v: any, i: number) => cache[i] !== v;
          while (lastCache.some(some)) {
            await aw(300);
            lastCache = cache;
            cache = getCacheValue(ref);
          }
          finish(ref);
        })();
      }
    };
    return func;
  }
  public tryCatch(call: () => any, handle: (e: Error) => any) {
    try {
      return call();
    } catch ( ex:unknown ) {
 const e = ex instanceof Error?ex:new Error(JSON.stringify(ex));
      return handle(e);
    }
  }
  public array2map<K, V, O>(arry: Array<O>, kf: (o: O) => K, vf: (o: O) => V = o => o as any): Map<K, V> {
    const map = new Map<K, V>();
    arry.forEach(o => map.set(kf(o), vf(o)));
    return map;
  }
  public relativeLayout(style: any, [posi, left, top]: [number] | [number, string] | [number, string, string]): any {
    const [width, height] = [style.width, style.height];
    if ([undefined, null, ""].includes(width)) {
      delete style.left;
    } else {
      style.left = lf(posi, width, left);
    }
    if ([undefined, null, ""].includes(height)) {
      delete style.top;
    } else {
      style.top = tp(posi, height, top);
    }
    return style;
  }
  public gradient(crnt: () => number, trgt: () => number, setter: (val: number) => void, timeout: number) {
    (async () => {
      if (timeout > 0) {
        const c = crnt();
        const t = trgt();
        if (c !== t) {
          const d = t - c;
          const v = d / Math.max(1, timeout / __perFpsMs);
          setter(c + v);
          await aw(Math.min(timeout - __perFpsMs, __perFpsMs));
          this.gradient(crnt, trgt, setter, timeout - __perFpsMs);
        }
      }
    })();
  }
  public splitProperties(path: string) {
    return path.replace(propertySplits(), "-").split("-").filter(Boolean);
  }
  public prxyImpl(proxy: any, impl: any) {
    let isSameFatherFound = false;
    for (let chain = impl; !isSameFatherFound && (!(proxy instanceof this.findLocalClass(chain)) || (isSameFatherFound = true)); chain = this.findSuperChain(chain)) {
      const keys = Object.getOwnPropertyNames(chain);
      for (let x of keys) {
        if (typeof chain[x] === "function" && !(proxy as Object).hasOwnProperty(x) && !this.findLocalChain(proxy).hasOwnProperty(x)) {
          proxy[x] = function () {
            return impl[x].apply(impl, Array.from(arguments));
          };
        }
      }
    }
  }
  public findLocalClass = (o: any): { new (): any } => {
    return this.findLocalChain(o).constructor;
  };
  public findSuperChain = (o: any): any => {
    return this.findLocalChain(o).__proto__;
  };
  public findLocalChain = (o: any): any => {
    if (o.constructor !== o.__proto__?.constructor) return o;
    else return o.__proto__;
  };
  public findSuperClass = (c: { new (): any }): { new (): any } => {
    if (c === Object) return c;
    else return c.prototype.__proto__.constructor;
  };
  public getQueryVariable(variable: string) {
    var query = window.location.search.substring(1);
    var vars = query.split("&");
    for (var i = 0; i < vars.length; i++) {
      var pair = vars[i].split("=");
      if (pair[0] === variable) {
        return pair[1];
      }
    }
    return false;
  }
  public getNameField(orefType: string): Field {
    const nameFields = Bi.propertyDescription.getProperties(orefType, "name") as string[];
    let defaultNameField: Field | null = null;
    if (!!nameFields && nameFields.length > 0) {
      return Bi.propertyDescription.get(orefType, nameFields[0]);
    } else if (!!(defaultNameField = Bi.propertyDescription.get(orefType, "name"))) {
      return defaultNameField;
    } else {
      throw new Error(`No name field in orefType [ ${orefType} ]`);
    }
  }
  public getNodeField(orefType: string): Field | undefined {
    return (Bi.propertyDescription.get(orefType) as Field[]).find(pd => /parent/.test(pd.name));
  }
  public async findOne(resourceName: string, id: string, property: string) {
    const orefType = Bi.propertyDescription.get(this.upperFirst(resourceName), property).orefType;
    const orefResourceName = this.lowerFirst(orefType || "");
    const oneId_res = await Bi.rpc.send(
      this.platingValue({
        action: "getlist", //e
        resourceName: resourceName,
        "data.criteria.criterions.0": { op: "eq", property: "id", value: id },
        "data.criteria.criterions.1": { op: "ne", property: property + ".id", value: null },
        "data.criteria.projections.0.property": property + ".id"
      })
    );
    let oneId = this.readValue(oneId_res, "data.list[0].values[0]");
    if (!oneId) {
      return { result: true, data: { data: null } };
    } else {
      return await Bi.rpc.send({ resourceName: orefResourceName, action: "getone", data: { id: oneId } });
    }
  }
  public async findMany(resourceName: string, id: string, property: string) {
    const orefType = Bi.propertyDescription.get(this.upperFirst(resourceName), property).orefType;
    const orefResourceName = this.lowerFirst(orefType || "");
    const orefNameProperty = this.getNameField(orefType || "").name;
    let res = await Bi.rpc.send(
      this.platingValue({
        action: "getlist",
        resourceName: resourceName,
        "data.criteria.criterions.0": { op: "eq", property: "id", value: id },
        "data.criteria.criterions.1": { op: "ne", property: property + ".id", value: null },
        "data.criteria.projections.0": { property: property + ".id" }
      })
    );
    if (res?.data.list.length > 0) {
      res = await Bi.rpc.send({
        action: "getlist", //e
        resourceName: orefResourceName,
        data: {
          criteria: {
            criterions: [{ op: "in", property: "id", value: res.data.list.map((it: any) => it.values[0]) }], //e
            projections: [{ property: "id" }, { property: orefNameProperty }]
          }
        }
      });
      return { ...res, data: { list: res.data.list.map((it: any) => ({ id: it.values[0], [orefNameProperty]: it.values[1] })) } };
    } else {
      return new RpcResult(true, { list: [] });
    }
  }
  public async findManyWithFullProperties(resourceName: string, id: string, property: string) {
    const orefType = Bi.propertyDescription.get(this.upperFirst(resourceName), property).orefType;
    const orefResourceName = this.lowerFirst(orefType || "");
    let res = await Bi.rpc.send({
      action: "getlist", //e
      resourceName: resourceName,
      data: { criteria: { criterions: [{ op: "eq", property: "id", value: id }], projections: [{ op: "group_concat(1)", property: property + ".id" }] } }
    });
    if (!!res?.data?.list[0]) {
      res = await Bi.rpc.send({
        action: "getlist",
        resourceName: orefResourceName,
        data: { criteria: { criterions: [{ op: "in", property: "id", value: res.data.list[0].values[0] }] } }
      });
    } else {
      res = new RpcResult(true, { list: [] });
    }
    return res;
  }
  public async readValueByProperty(orefType: string, root: any, propertyPath: string) {
    let res = root || null;
    let properties = this.splitProperties(propertyPath);
    let objType = orefType;
    for (let i = 0; i < properties.length; i++) {
      const property = properties[i];
      const field = Bi.propertyDescription.get(objType, property) as Field;
      if (res.hasOwnProperty(property)) {
        res = res[property];
      } else if (field.izArray) {
        res = await this.findManyWithFullProperties(objType, res.id, property);
        res = res.data.list;
      } else {
        res = await this.findOne(objType, res.id, property);
        res = res.data.data;
      }
      if (!field.orefType) {
        Throwable(`Impossible Error: field.orefType is ${field.orefType}`);
      } else {
        objType = field.orefType;
      }
    }
    return res;
  }
  public async loadValueByProperty(orefType: string, root: any, property: string) {
    return (root[property] = await this.readValueByProperty(orefType, root, property));
  }
  public isFieldEditable(field: Field, data: any) {
    return !(
      field.mode.readOnly ||
      !!field.disabled || //e
      (!!data && !!data.id && !field.mode.updatable) || //e
      (!!data && !data.id && !field.mode.insertable)
    );
  }
  public once<T extends Function>(func: T): T {
    let executed = false;
    const funcx: any = function (this: any) {
      const args = Array.from(arguments);
      if (!executed && (executed = true)) {
        func.apply(this, args as any);
      }
    };
    return funcx as T;
  }
  public isEditable(field: Field, data: any) {
    return !(
      field.mode.readOnly || //e
      field.disabled || //e
      (!!data && !!data.id && !field.mode.updatable) || //e
      (!(!!data && !!data.id) && !field.mode.insertable)
    ); //e
  }
  public cacheFunc<T extends Function>(func: T): T {
    const resultCache = new Map<string, any>();
    const newFunc: any = function (this: any) {
      const args = Array.from(arguments);
      const key = args.reduce((a, b) => `${a}_${b}`);
      let result: any;
      if (resultCache.has(key)) {
        result = resultCache.get(key);
      } else {
        try {
          result = func.apply(this, args);
        } catch ( ex:unknown ) {
 const e = ex instanceof Error?ex:new Error(JSON.stringify(ex));
          result = e;
        }
        resultCache.set(key, result);
      }
      if (result instanceof Error) {
        throw result;
      } else {
        return result;
      }
    };
    return newFunc as T;
  }

  public async comfirm(content: any) {
    const pending = pendingResolve<boolean>();
    Bi.dialog.on(
      <Ei.Layout params={[]}>
        {content}
        <Ei.Layout params={[]}>
          <Ei.Button params={[]} onClick={this.linkFuncs(() => pending.resolve(true), Bi.dialog.off)}>
            {"Yes"}
          </Ei.Button>
          <Ei.Button params={[]} onClick={this.linkFuncs(() => pending.resolve(false), Bi.dialog.off)}>
            {"No"}
          </Ei.Button>
        </Ei.Layout>
      </Ei.Layout>
    );
    return pending;
  }
  public isAsync(func: Function) {
    const key = "eval";
    const eva = window[key];
    return func.constructor === eva("(async ()=>{}).constructor");
  }
}
export const order = () => consts.firstOrder;
const __parseProperty = (property: string) => (/[1-9]*[0-9]/.test(property) ? Number.parseInt(property) : property);

const S4 = function () {
  return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
};
const emptyFunc = function () {};
// const __delayMs = 100;
const __FPS = 120;
const __perFpsMs = Math.round(1000 / __FPS) - 1;
const [mid, end] = [(size: any) => ([0, "0"] === size ? `50%` : `50% - ${size} / 2`), (size: any) => ([0, "0"] === size ? `100%` : `100% - ${size}`)];
const sftp = (a: (p: number) => boolean, b: (p: number) => boolean) => (p: number, s: any, m: any) => `calc(${a(p) ? "0px" : b(p) ? mid(s) : end(s)}${!!m ? ` + ${m}` : ""})`;
const [lf, tp] = [sftp.apply(null, [p => p % 3 === 1, p => p % 3 === 2]), sftp.apply(null, [p => p < 4, p => p < 7])];
const propertySplits = () => new RegExp("(\\]\\.)|(\\]\\[)|(\\[)|(\\])|(\\.)", "g");
